We will write software for all these steps. Normally
I have seen the process written from the bottom up. I think it is in fact easier
to understand if we go from the top down.
SOFTWARE DOWNLOADS
All the software mentioned on this page can be downloaded from the links at the
bottom of this page.
They contain my most recent updates and may not
correspond exactly with the text here.
Also in this particular case, I am
including the Altair CPM
Simulator in the same folder so people can run the system
immediately on
windows.
Double click on altair80.exe to launch the program. This will create
a SIMH specific DOS box window.
At the "A>" prompt within that window type:-
do cpm3
After the carriage return you will see a completely self contained and
functional CPM3 system. The box will look like this:-
![Altair Signon](Altair Signon.jpg)
The simulator behaves like a ~50MHz Z80 driven system. For example
to see the user 0 directory type:-
DIR
You will see the normal CPM3 directory listing, something like this:-
![DIR listing](../Altair Simmulator/Dir Listing.jpg)
You can read up on CPM3 to run many of these programs. Other "well known"
programs have been added as well. They are marked as CPM3 "System files" so they
can be run from any drive and user group.
For our needs here however, we will take advantage of the fact that the simulator has
an large memory disk "I:" drive that we will normally use as our work
disk. So typing "I:" CR will bring you to that
drive.
You should see:
I:>
Within that drive will be already the relevant files to run the
CPM3 files discussed below. This makes it a one click process for
beginners, however unfortunately the combined set of CPM files and CPM3
simulator is quite large (~4GB) because it contains many other CPM programs not
used here. For further CPM3 software downloads on other pages I will just
supply the source code and you can splice in the simulator yourself as describe
here.
OK lets get started. Starting form the top item in the above diagram:-
THE EPROM BOOT SECTOR LOADER.
This small piece of 8080/Z80 code resides in your boot monitor. Because
the amount of space in a ROM monitor is small (typically 4K) this code has to
be simple and compact. Also because this same "Boot Loader" may be used to read
in different operating systems (CPM 1.4, CPM2.2, CPM 3, DOS etc) or
different disk sizes/densities, we need a
flexible way of reading in a different number of sectors from a disk. We do this
in a two step process. We read in (always) the first sector to 80H in RAM, jump
to that location which will have specific code on that disk for
that operating system to determine how many further disk
sectors are to be read. These sectors are then read in by further code in the
ROM monitor placing them at (for CPM), 100H in RAM. The ROM monitor
code then removes itself from the process and transfers all control to CPM
starting at 100H in RAM.
The code at 100H is RAM is really a stripped down very compact CPM operating
system (called CPMLDR.COM) that's only function in life is to read is a
single CPM file on the disk named CPM3.SYS. It has to be a "proper" CPM
operating system however because the CPM3.SYS file is a "normal" CPM file and can
exist anywhere on the disk. In a Non-Banked system this CPM3.SYS
code is placed in the top of RAM. In a Banked system much of it is placed in
another RAM bank - thus freeing up much of the TPA. More on that later.
We are almost home. The last step in the process is the CPM3.SYS code
itself reads in a further CPM3 file called CCP.COM. This Console Command
Processor file has the code that links the operating system with the
outside world (console, printer etc). When this is done the CPM3.SYS code
transfers control to the CCP.COM code and the A:> appears.
This my seem a convoluted way of doing things -- and to some extent it is,
however what is really nice is that once you have the original CPMLDR working
you can easily and quickly make changes to the operating system by placing a new
CPM3.SYS file on the disk. If you change the name (CPM always looks for
exactly "CPM3.SYS") you can have different hardware arrangements stored on the
same disk.
Note in the earlier versions of CPM (1.4, 2.2 etc) the process was much
simpler. The first boot sector code caused the ROM monitor to load the
remaining sectors of CPM code in high memory and just jump to that location.
Any time a change was made to the BIOS however, the whole system had to be
rebuilt.
Now let us look at an example of the EPROM boot loader code. My complete
"MASTER.Z80" EEPROM code can be see
here. The relevant
BIOS loader code begins at ZFDC_BOOT: This is the
monitor I use with the S100Computers/N8VRAM
Z80 CPU
board. It utilizes the simple command driven interface to read disk
sectors into RAM utilizing the
ZFDC board.
;---------------------- ZFDC FDC EEPROM BOOT LOADER -----------------------------------
ZFDC_BOOT: ;Cold Boot with ZFDC FDC Board
OUT RESET_ZFDC_PORT,A ;Do a ZFDC board hardware reset. Does not matter what is in [A]
LD A,STATUS_DELAY ;~0.5 second at 10 MHz
LD BC,0 ;Delay to allow board to setup hardware
WAIT_D: DEC B
JR NZ,WAIT_D ;Delay for ~0.5 seconds
DEC B ;Reset B to 0FFH
DEC C
JR NZ,WAIT_D
DEC A
JR NZ,WAIT_D
IN A,S100_DATA_B ;Check the board is there
CP A,CMD_HANDSHAKE ;Make sure we get HANDSHAKE byte back
JP NZ,ERR_NR ;If error, just abort
LD A,CMD_HANDSHAKE ;Send another byte just to be sure.
OUT S100_DATA_B,A ;This clears up ints on ZFDC board
CALL WAIT_FOR_ACK ;Wait to make sure all is well.
OR A,A
JP NZ,ERR_NR ;If error, just abort
LD C,CMD_SET_FORMAT ;Send Set Disk Format to 8" SSSD DISK
CALL S100OUT
LD C,0 ;Floppy Drive 0, (ZFDC Board expects a 0H, 1H, 2H or 3H)
CALL S100OUT
LD C,STD8IBM ;ZFDC Board expects a Disk Format Table Number (0,1,2...13H)
CALL S100OUT
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
JP NZ,ERR_NR ;If error, just abort
LD C,CMD_SET_DRIVE ;Send a "Set Drive CMD" to ZFDC board
CALL S100OUT
LD C,0 ;Floppy Drive #, (ZFDC Board expects a 0H, 1H, 2H or 3H)
CALL S100OUT
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
JP NZ,ERR_NR ;If error, just abort
;Drive selected and ready to read sectors. Note this code
;is written to be compatible with the boot loader for the
;Versafloppy-II disk controller as well.
LD A,STDSDT ;SETUP FOR SD
LD (@COUNT),A ;STORE AS 26 SECTORS/TRACK
XOR A ;Setup Boot Sector read track
LD (@TRK),A
INC A
LD (@SCTR),A
LD HL,COLD ;Will load the boot sector to 80H in RAM
LD (@TADDR),HL
CALL ZFDC_READ_SECTOR
JP NZ,ERR_LD
LD HL,COLD ;Check the load went OK.
LD A,(HL)
CP 31H ;EXPECT TO HAVE 31H @80H IE. LD SP,80H
JP Z,COLD ;AS THE FIRST INSTRUCTION. IF OK JP 80H
JP ERR_LD1 ;Boot Sector Data incorrect
ZFDC_READ_SECTOR: ;CORE code to read a sector with ZFDC board
LD C,CMD_SET_TRACK ;Set Track
CALL S100OUT
LD A,(@TRK)
LD C,A
CALL S100OUT ;Send Selected track HEX number
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
JP NZ,ERR_NR ;If error, just abort
LD C,CMD_SET_SECTOR ;Set Sector # to side A (or for DS disks also side B)
CALL S100OUT
LD A,(@SCTR)
LD C,A
CALL S100OUT ;Send Selected sector HEX number
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
JP NZ,ERR_NR ;If error, just abort
LD C,CMD_SEEK_TRACK ;Later can let board do this
CALL S100OUT
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
JP NZ,ERR_NR ;If error, just abort
LD C,CMD_READ_SECTOR ;Routine assumes required Drive Table,Drive,Side,Track, and sector are already sent to board
CALL S100OUT ;(Note [HL]-> Sector DMA address)
CALL WAIT_FOR_ACK ;Wait for NO_ERRORS_FLAG to come back
JP NZ,ERR_NR ;If error, just abort
LD HL,(@TADDR) ;Set DMA address
LD DE,(@SEC_SIZE) ;For CPM this will be 128 Byte sector(s)
RD_SEC:CALL S100IN ;Note potential to lockup here & below (but unlightly)
LD (HL),A
INC HL
DEC DE
LD A,E
OR A,D
JR NZ,RD_SEC
CALL WAIT_FOR_ACK ;Return Z (and NO_ERRORS_FLAG in [A]), or NZ with error # in [A]
RET
S100OUT:
IN A,S100_STATUS_B ;Send data to ZFDC output (arrive with character to be sent in C)
BIT DIRECTION_BIT,A ;Is ZFDC in output mode, if not wait
JR NZ,S100OUT
BIT DATA_OUT_RDY,A ;Has previous (if any) character been read.
JR Z,S100OUT ;Z if not yet ready
LD A,C
OUT S100_DATA_B,A
RET
S100STAT:
IN A,S100_STATUS_B ;Check if ZFDC has any data for S-100 system
BIT DATA_IN_RDY,A ;Anything there ?
RET Z ;Return 0 if nothing
XOR A,A
DEC A ;Return NZ, & 0FFH in A if something there
RET
S100IN:
IN A,S100_STATUS_B ;Check if ZFDC has any data for S-100 system
BIT DIRECTION_BIT,A ;Is ZFDC in input mode, if not wait
JR Z,S100IN ;If low then ZFDC board is still in input mode, wait
BIT DATA_IN_RDY,A
JR Z,S100IN
IN A,S100_DATA_A ;return with character in A
RET
WAIT_FOR_ACK: ;Delay to wait for ZFDC to return data. There is a timeout of about 2 sec.
PUSH BC ;This can be increased if you are displaying debugging info on the ZFDC
PUSH DE ;HEX LED display.
LD BC,0
LD E,STATUS_DELAY ;Timeout, (about 2 seconds)
WAIT_1: IN A,S100_STATUS_B ;Check if ZFDC has any data for S-100 system
BIT DIRECTION_BIT,A ;Is ZFDC in input mode
JR Z,WAIT_2 ;if low then ZFDC is still in input mode
CALL S100STAT ;Wait until ZFDC Board sends something
JR Z,WAIT_2
CALL S100IN ;Get returned Error # (Note this releases the SEND_DATA routine on the ZFDC board)
CP A,NO_ERRORS_FLAG ;Was SEND_OK/NO_ERRORS_FLAG sent back from ZFDC Board
POP DE ;Balance up stack
POP BC
RET ;Return NZ if problem, Z if no problem
WAIT_2: DEC B
JR NZ,WAIT_1 ;Try for ~2 seconds
DEC B ;Reset B to 0FFH
DEC C
JR NZ,WAIT_1
DEC B ;Reset B to 0FFH
DEC C
DEC E
JR NZ,WAIT_1
XOR A,A
DEC A
POP DE ;Balance up stack
POP BC
RET ;Return NZ flag set if timeout AND 0FFH in [A]
As I explained above, we have just read in one sector to RAM at 80H. The
next piece of code is the EEPROM code to read in the next (in our case) 51
sectors from Track 0 and 1 on the 8" floppy disk. The simple EEPROM code
to do this is shown here:-
; LOAD A NUMBER OF SECTORS ;Note this loader will be particularly slow since the sector
;reads are not skewed. (Actually one rotation/sector)!
ZFDC_LOADER: ;Loader with ZFDC FDC Board
CALL ZFDC_READ_SECTOR
JP NZ,ERR_LD
LD C,'.' ;Show progress
CALL CO
CALL INCP ;Increment sector, track adjust NREC
JR NZ,ZFDC_LOADER
RET
; INC SECTOR AND TRACK
INCP: LD HL,(@TADDR)
LD DE,(@SEC_SIZE) ;128 or 512 byte sectors
INCP2: ADD HL,DE
LD (@TADDR),HL
LD HL,@NREC
DEC (HL)
RET Z ;Return when we have done all sectors (~51)
LD HL,@SCTR
INC (HL)
LD A,(@COUNT) ;IS ONE TRACK DONE YET (Sec/track+1)
INC A
CP (HL)
RET NZ ;IF FULL Z, THEN GO TO NEXT TRACK
LD (HL),1 ;SET SECTOR COUNT BACK TO 1
INC HL ;ASSUMES @TRK=SECTOR+1 IE 44H
INC (HL)
OR A ;MAKE SURE TO RETURN NZ
RET
The key variable is @NREC. This is the number the
first sector code placed in RAM (at 45H) to tell this module how many further
sectors to read in. Note how the ZFDC_LOADER routine just does a simple
return when finished. Its again is up to the code from the first sector to then
jump to 100H in RAM.
THE EPROM CPMLDR LOADER.
Next we will write the CPM Loader program
CPMLDR.COM. This is actually the hardest part of the software to write.
We are is essence writing a primitive CPM3 BIOS. The good news is we don't worry
about memory banking, and only have to take care of two major BIOS functions,
reading sectors from (only) the boot disk and writing to the console.
You don't even need console input -- though it is useful for debugging.
The complete Loader BIOS for the ZFDC board utilizing an 8" single density 128
byte sector disk (ZLDRBIOS.Z80) can be seen
here.
Before we get into it, we need to discuss how CPM3 understands disk formats.
In CPM3 each disk has its own Disk Parameter
Block table. Unfortunately
these tables (DPB's) are fairly complex and rather
than repeat everything here, you should read the Digital Research
CPM3
System Guide
mentioned above. They contain byte and word values to define areas in RAM
for sector skew translation, directory buffers and hash tables etc.
We will not worry about any of this now because Digital Research provides a
series of Assembler macros that builds these tables automatically for you.
For our 8" disk the DPB macro is:-
Floppy$DPB: DPB
128,26,77,1024,64,2
128 = Bytes per sector
26 = Sectors per track
77 = Tracks per disk
1024 = The allocation unit size (1K blocks
for an 8" disk)
64 = Maximum number of directory entries on
a disk
2 = Number or
tracks reserved for the CPM operating system.
Tracks start at 0,1,2,3,.. so tracks 0 & 1 are for the operating system. The disk
directory starts on Track 2.
You will see this macro at the bottom of the above code listing.
Next we need another table which Digital Research calls a
Disk Parameter Header table or
DPH. This table is somewhat simpler and
contains amongst other things a pointer to the above DPB table.
Again an assembler macro is supplied. For our 8" disk the DPH macro is:-
DPH0: DPH
SKEW6,Floppy$DPB,16,31
SKEW6
Is a pointer to another macro (described below) that describes how the sectors
are numbered on a track.
Floppy#DPB Is a word pointer to the
above DPB for the 8" floppy disk.
16
This is the maximum size in bytes of the disk checksum vector, lets skip for now
31
This is the maximum size in bytes of the disk allocation vector, lets skip for
now also and just use these values
The SKEW6 pointer, refers to a macro that describes the order of how sectors are
arranged on a disk in terms of sector numbering. If sectors were numbered
1,2,3,4..., sequential sector reading would be slow because once one sector has
been read, processed and placed in RAM by the CPU the head has already moved
along a few more sectors on the disk. The system would have to wait an almost
complete rotation for next physical sector to come around. By
"skewing" the sectors this is avoided. For example on a standard 8" IBM
disk the order is:-
01H,07H,0DH,13H,19H,05H,0BH,11H,17H,03H,09H,07H,15H,02H,08H,0EH,14H,1AH,06H,0EH,12H,18H,04H,0AH,10H,16H
Again to save you the hassle of figuring the order out Digital Research provided
a macro:-
SKEW6: SKEW 26,6,0
SKEW26 Is the total number of sectors per
track for that disk
6
This is the skip number for the skew.
0
This is the number of the first sector on the disk.
It turns out that the last number often causes problems. It is usually a 0 or 1.
If you set it to 0 then for all floppy disks in the actual disk "set sector"
code before you send the data to the disk you must increment the value by one.
This is because sectors on floppies are numbered 1,2,3.....
You can set the above to SKEW 26,6,1 and not do this, however its really
important throughout all your BIOS code to be consistent. As we shall see the CF
card/IDE drive BIOS'es number sectors 0,1,2,3 etc. In all my code I
use the 26,6,0 format. I lost a lot of time in the past
tracking a bug like this down! Your disk will appear fine initially but get
messed up later when you go back and forth between disk formats.
OK we are almost there. There is still yet one more table. This is the
Disk Drive Table or
DTBL
This one is simple. CPM3 allows for up to 16 different drives. The
DPH table is just a list of pointers to each DPH for each drive. Any entries
with no drive are set to 0. We have only one drive here.
The DTBL will be:-
@DTBL: DW DPH0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
When you look at the bottom of the code for the CPMLDR "BIOS" (ZLDRBIOS.Z80)
you will see all of the above combined.
Now in order to have a functional basic operating system we need to splice in
the Digital Research CPMLDR.REL file. This a
disk operating system file supplied by Digital Research that works with our
basic custom BIOS. Remember all we are trying to do here is read in
the main CPM3.SYS file on the disk. The CPMLDR.REL file is set to run at
100H in RAM. It is constructed such that it expects immediately
above it the Loader BIOS jump vectors exactly as they are at the start of
ZLDRBIOS.ASM.
We use the CPM program LINK to splice the two sections together and write them
out as one file CPMLDR.COM.
If you load this program with SID or ZSID and jump to 100H in RAM the above code
should work and come back telling you it cannot locate the CPM3.SYS file on the
disk. (Because we have not done it yet).
However before we get to that we have to do one other thing. We need to get some
way to write the CPMLDR.COM file to tracks 0 and 1 of the floppy disk.
I have written the program ZSYSGEN.Z80 to do this.
It can be seen here. Its essentially a reverse of the EPROM
monitor code to write (rather than read) multiple sectors to the disk.
Finally we need to splice the ZSYSGEN and CPMLDR code together to yield one
"standard" CPM program which I call
ZSYSGEN.COM.
This program on any standard IBM 8" floppy disk will place the above
CPMLDR.com code on the system tracks.
The code is a little bit more complicate than I described above because it has
the option of placing a Banked or Non-Banked CPMLDR.COM file on the disk. For
now everything will be for a non-banked system.
All of the above assembly and splicing can be done automatically by running the
ZSYSGEN.SUB file. This can be see
here.
THE CPM3.SYS FILE.
This file is the heart of the operating system. First we need to decide
how we will layout our hardware. For our first system we will just have a
Console and two IBM format 8" floppy disks A:& B:.
First we will work on the Floppy disk portion of the BIOS. This is contained in
the file 8FL3.ASM. The source code can be
seen here. The BIOS contains
essentially the same tables that I described above and uses the same assembler
macros. However there is one more new table structure it is called the
Extended Disk Parameter Table
(XDPH). The XDPH is in fact nothing more than the above DPH table
except that additional bytes and word parameters are placed immediately before
and after the "regular" DPH table described above. However the exact
placement of these extra parameters is very critical. Here is the XDPH
table I use for drive A:
; EXTENDED DISK PARAMETER HEADER FOR DRIVE 0: (A:)
DW WRITE$SECTOR ;FD SEC WRITE ROUTINE
DW READ$SECTOR ;FD SEC READ ROUTINE
DW FLOPPY$LOGIN$0 ;FLOPPY DISK "A:" LOGIN PROCEDURE
DW FLOPPY$INIT$0 ;FLOPPY DISK "A:" DRIVE INITIALIZATION ROUTINE
DB 0 ;RELATIVE DRIVE 0 ON THIS CONTROLLER
DB STD8IBM ;MEDIA TYPE KNOWN SSSD 8"
;HI BIT SET : DRIVE NEEDS RECALIBRATING
DPH0: DPH SD128$trans,SDSS128$dpb,,
;Bytes 0-24 used by DPH/CPM
DW 128 ;25, 128 Bytes per sector count
DB 0 ;27, Drive Hardware Select
You can see the normal DPH begins at DOH0: Listed above it are two byte
fields (unused here) that the software can use as flags for disk density/format
etc. CPM does not use them. Then immediately above them are four word
pointers to disk the initialization, login, sector read and write routines.
These are the routines you must custom write for each disk. CPM also
adds table values to the bottom of the DPH. In fact from the location DPH0: the
next 24 bytes must never be changed by your custom BIOS. You can add any other
flags or pointers you like below that. I use two in all my floppy disk BIOS'es.
one to hold the sector byte count for that disk format, the other the actual
hardware select byte for that drive.
The relevance of all these flags and pointers being placed at these precise
positions relative to DPH0: is that throughout CPM3, all sector reads and writes
will supply a pointer to the relevant drives DPH in the register pair [DE].
Using the following code:-
PUSHIX ;Save [IX]
PUSH D ;[DE]->[IX]
POPIX ;get XDPH address for current requested drive to [ix]
we can utilize the useful Z80 [IX] register to obtain, compare, or change values
within the XDPH table. You will see a few examples where I have done this
in this simple BIOS. For example the bytes/sector for any disk is
[IX+25].
Take a look over the 8FL3.ASM code to get
comfortable with the above. Don't worry about all the equates at
the start, most
are not used.
You should by now be familiar with the disk DPH, DPB and SKEW macros.
Next we need to look at the 8DRVTBL2.ASM file.
This one is simple. It just contains the DTBL we described above. However
now we have a total of two disks whose DPH's are labeled DPH0 & DPH1 so we see:-
@DTBL: DTBL
<DPH0,DPH1,0,0,0,0,0,0,0,0,0,0,0,0,0,0>